/**
 * parse.c - parse incoming messages and do something useful with them.
 *
 * woodpecker
 *
 * Copyright (C) 2003, Ethan Blanton <eblanton@cs.purdue.edu>
 * Copyright (C) 2008, The Woodpecker Team
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * 
 * $Id: parse.c 135 2008-05-03 06:16:16Z dubkat $
 * 
 */

#include "accountopt.h"
#include "conversation.h"
#include "notify.h"
#include "debug.h"
#include "cmds.h"
#include "pecker.h"
#include "helpers.h"

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>

static char *pecker_send_convert(struct pecker_conn *pecker, const char *string);
static char *pecker_recv_convert(struct pecker_conn *pecker, const char *string);

static void pecker_parse_error_cb(struct pecker_conn *pecker, char *input);

/* typedef void (*IRCMsgCallback)(struct pecker_conn *pecker, char *from, char name, char **args); */

/* Actions based on data sent from the server (mostly numerics) */
static struct _pecker_msg
{
    char *name;
    char *format;
    void (*cb) (struct pecker_conn * pecker, const char *name, const char *from, char **args);

} _pecker_msgs[] =
{
	/* *INDENT-OFF* */
	/* NUM    FMT         CALLBACK                            COMMENT */
    { "001",  "n:",       pecker_msg_endmotd          },   /* login ok */
    { "205",  "nnvvvvv",  pecker_msg_trace            },   /* trace response */
    { "263",  "n:",       pecker_msg_list_busy        },   /* Server load to heavy */
    { "301",  "nn:",      pecker_msg_away             },   /* User is away */
    { "311",  "nnvvv:",   pecker_msg_whois            },   /* Whois user */
    { "312",  "nnv:",     pecker_msg_whois            },   /* Whois server */
    { "313",  "nn:",      pecker_msg_whois            },   /* Whois peckerop */
/*  { "315",  "nc:",      pecker_msg_who              },*/ /* End of WHO list */
    { "317",  "nnvv",     pecker_msg_whois            },   /* Whois idle */
    { "321",  "*",        pecker_msg_list             },   /* Start of list */
    { "322",  "ncv:",     pecker_msg_list             },   /* List */
    { "323",  ":",        pecker_msg_list             },   /* End of list */
    { "341",  "nvc:",     pecker_msg_invite           },   /* Invited */
/*  { "352",  "nc:",      pecker_msg_who              },*/ /* WHO list */
/*  { "353",  "nvc:",     pecker_msg_names            },*/ /* Names list */
/*  { "366",  "nc:",      pecker_msg_names            },*/ /* End of names */
    { "376",  "n:",       pecker_msg_endmotd          },   /* End of MOTD */
    { "401",  "nc:",      pecker_msg_nonick_chan      },   /* No such channel */
    { "403",  "nc:",      pecker_msg_nochan           },   /* No such channel */
    { "404",  "nt:",      pecker_msg_nosend           },   /* Cannot send to chan */
    { "421",  "nv:",      pecker_msg_unknown          },   /* Unknown command */
    { "422",  "nv:",      pecker_msg_endmotd          },   /* No MOTD available */
    { "442",  "nc:",      pecker_msg_notinchan        },   /* Not in channel */
    { "471",  "nc:",      pecker_msg_chanfull         },   /* Channel Full */
    { "668",  "n:",       pecker_msg_login_failed     },   /* Invalid Authentication */
    { "690",  "ncnt:",    pecker_msg_richnames_list   },   /* Gay.com's RPL for names list */
    { "695",  "nc:",      pecker_msg_toomany_channels },   /* Too many channels (2) maximum joined */
    { "696",  "nc:",      pecker_msg_pay_channel      },   /* User tried to enter pay channel, rejected */
    { "698",  "nc:",      pecker_msg_create_pay_only  },   /* Creating a new room is a feature for paying member */
    { "701",  "nt:",      pecker_msg_no_such_nick     },   /* Tried to get whois for someone not on */
    
    { "invite",  "n:",    pecker_msg_invite  },  /* Invited */
    { "join",    "c:",    pecker_msg_join    },  /* Joined a channel */
    { "nick",    ":",     pecker_msg_nick    },  /* Nick change */
    { "notice",  "t:",    pecker_msg_notice  },  /* NOTICE recv */
    { "part",    "::",    pecker_msg_part    },  /* Parted a channel */
    { "ping",    ":",     pecker_msg_ping    },  /* Received PING from server */
    { "pong",    "v:",    pecker_msg_pong    },  /* Received PONG from server */
    { "privmsg", "t:",    pecker_msg_privmsg },  /* Received private message */
    { "quit",     ":",    pecker_msg_quit    },  /* QUIT notice */
    { NULL,       NULL,   NULL }
    /* *INDENT-ON* */
};

/* these are the commands that users can issue via /cmd */
static struct _pecker_user_cmd
{
    char *name;
    char *format;
    IRCCmdCallback cb;
    char *help;

} _pecker_cmds[] =
{
    /* *INDENT-OFF* */
    /* NAME     FMT   CALLBACK                HELP TEXT */
    { "action", ":",  pecker_cmd_ctcp_action, "action &lt;action to perform&gt;:  Perform an action." },
    { "away",   ":",  pecker_cmd_away,        "away [message]:  Set an away message, or use no message to return from being away." },
    { "j",      "cv", pecker_cmd_join,        "j &lt;room1&gt;[,room2][,...] [key1[,key2][,...]]:  Enter one or more channels, optionally providing a channel key for each if needed." },
    { "join",   "cv", pecker_cmd_join,        "join &lt;room1&gt;[,room2][,...] [key1[,key2][,...]]:  Enter one or more channels, optionally providing a channel key for each if needed." },
    { "me",     ":",  pecker_cmd_ctcp_action, "me &lt;action to perform&gt;:  Perform an action." },
    { "msg",    "t:", pecker_cmd_privmsg,     "msg &lt;nick&gt; &lt;message&gt;:  Send a private message to a user (as opposed to a channel)." },
    { "list",   "*",  pecker_cmd_list,        "list [filter]: List all channels, or if filter is specified, list channels containing filter text." },
/*  { "names",  "c",  pecker_cmd_names,       "names [channel]:  List the users currently in a channel." }, */
    { "nick",   "n",  pecker_cmd_nick,        "nick &lt;new nickname&gt;:  Change your nickname." },
    { "part",   "c:", pecker_cmd_part,        "part [room] [message]:  Leave the current channel, or a specified channel, with an optional message." },
    { "ping",   "n",  pecker_cmd_ping,        "ping [nick]:  Asks how much lag a user (or the server if no user specified) has." },
    { "query",  "n:", pecker_cmd_query,       "query &lt;nick&gt; &lt;message&gt;:  Send a private message to a user (as opposed to a channel)." },
    { "quit",   ":",  pecker_cmd_quit,        "quit [message]:  Disconnect from the server, with an optional message." },
    { "quote",  "*",  pecker_cmd_quote,       "quote [...]:  Send a raw command to the server." },
/*  { "umode",  ":",  pecker_cmd_mode,        "umode &lt;+|-&gt;&lt;A-Za-z&gt;:  Set or unset a user mode." }, */
    { "invite", "cv", pecker_cmd_invite,      "invite [user] [channel]: Invite a user to a channel." },
    { "trace",  "n",  pecker_cmd_trace,       "trace &lt;nick&gt;: Get trace information on a user." },
    { "whois",  "n",  pecker_cmd_info,        "whois &lt;nick&gt;:  Get information on a user." },
    {  NULL,    NULL, NULL }
    /* *INDENT-ON* */

};


static PurpleCmdRet
pecker_parse_purple_cmd(PurpleConversation * conv, const gchar * cmd, gchar ** args, gchar ** error, void *data)
{
    PurpleConnection *gc;
    struct pecker_conn *pecker;
    struct _pecker_user_cmd *cmdent;

    gc = purple_conversation_get_gc(conv);
    if(!gc)
        return PURPLE_CMD_RET_FAILED;

    pecker = gc->proto_data;

    if((cmdent = g_hash_table_lookup(pecker->cmds, cmd)) == NULL)
        return PURPLE_CMD_RET_FAILED;

    (cmdent->cb) (pecker, cmd, purple_conversation_get_name(conv), (const char **) args);

    return PURPLE_CMD_RET_OK;
}

static void
pecker_register_command(struct _pecker_user_cmd *c)
{
    PurpleCmdFlag f;
    char args[10];
    char *format;
    int i;

    f = PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM | PURPLE_CMD_FLAG_PRPL_ONLY | PURPLE_CMD_FLAG_ALLOW_WRONG_ARGS;

    format = c->format;

    for (i = 0; (i < (sizeof(args) - 1)) && *format; i++, format++)
        switch (*format)
        {
        case 'v':
        case 'n':
        case 'c':
        case 't':
            args[i] = 'w';
            break;
        case ':':
        case '*':
            args[i] = 's';
            break;
        }

    args[i] = '\0';

    purple_cmd_register(c->name, args, PURPLE_CMD_P_PRPL, f, "prpl-pecker", pecker_parse_purple_cmd, c->help, NULL);
}

void
pecker_register_commands(void)
{
    struct _pecker_user_cmd *c;

    for (c = _pecker_cmds; c && c->name; c++)
        pecker_register_command(c);
}

static char *
pecker_send_convert(struct pecker_conn *pecker, const char *string)
{
    char *utf8;
    GError *err = NULL;
    const gchar *charset;

    charset = purple_account_get_string(pecker->account, "encoding", IRC_DEFAULT_CHARSET);
    if(!strcasecmp("UTF-8", charset))
        return g_strdup(string);

    utf8 = g_convert(string, strlen(string), charset, "UTF-8", NULL, NULL, &err);
    if(err)
    {
        purple_debug(PURPLE_DEBUG_ERROR, "pecker", "Send conversion error: %s\n", err->message);
        purple_debug(PURPLE_DEBUG_ERROR, "pecker", "Sending as UTF-8 instead of %s\n", charset);
        utf8 = g_strdup(string);
        g_error_free(err);
    }

    return utf8;
}

static char *
pecker_recv_convert(struct pecker_conn *pecker, const char *string)
{
    char *utf8 = NULL;
    GError *err = NULL;
    const gchar *charset;

    charset = purple_account_get_string(pecker->account, "encoding", IRC_DEFAULT_CHARSET);

    if(!strcasecmp("UTF-8", charset))
    {
        if(g_utf8_validate(string, strlen(string), NULL))
            utf8 = g_strdup(string);
    }
    else
    {
        utf8 = g_convert(string, strlen(string), "UTF-8", charset, NULL, NULL, &err);
    }

    if(err)
    {
        purple_debug(PURPLE_DEBUG_ERROR, "pecker", "recv conversion error: %s\n", err->message);
        g_error_free(err);
    }

    if(utf8 == NULL)
        utf8 = g_strdup("(There was an error converting this message.  Check the 'Encoding' option in the Account Editor)");

    return utf8;
}


char *
pecker_parse_ctcp(struct pecker_conn *pecker, const char *from, const char *to, const char *msg, int notice)
{
    PurpleConnection *gc;
    const char *cur = msg + 1;
    char *buf, *ctcp;
    time_t timestamp;

    /* Note that this is NOT correct w.r.t. multiple CTCPs in one message and low-level quoting ... 
       but if you want that crap, use a real IRC client. */

    if(msg[0] != '\001' || msg[strlen(msg) - 1] != '\001')
        return g_strdup(msg);

    if(!strncmp(cur, "ACTION ", 7))
    {
        cur += 7;
        buf = g_strdup_printf("/me %s", cur);
        buf[strlen(buf) - 1] = '\0';
        return buf;
    }
    else if(!strncmp(cur, "PING ", 5))
    {
        if(notice)
        {                       /* reply */
            sscanf(cur, "PING %lu", &timestamp);
            gc = purple_account_get_connection(pecker->account);
            if(!gc)
                return NULL;
            buf = g_strdup_printf("Reply time from %s: %lu seconds", from, time(NULL) - timestamp);
            purple_notify_info(gc, "PONG", "CTCP PING reply", buf);
            g_free(buf);
            return NULL;
        }
        else
        {
            buf = pecker_format(pecker, "vt:", "NOTICE", from, msg);
            pecker_send(pecker, buf);
            g_free(buf);
            gc = purple_account_get_connection(pecker->account);
        }
    }
    else if(!strncmp(cur, "VERSION", 7) && !notice)
    {
        buf = pecker_format(pecker, "vt:", "NOTICE", from, "\001VERSION WoodPecker for Pidgin IRC\001");
        pecker_send(pecker, buf);
        g_free(buf);
    }
    else if(!strncmp(cur, "DCC SEND ", 9))
    {
        // pecker_dccsend_recv(pecker, from, msg + 10);
        return NULL;
    }

    ctcp = g_strdup(msg + 1);
    ctcp[strlen(ctcp) - 1] = '\0';
    buf = g_strdup_printf("Received CTCP '%s' (to %s) from %s", ctcp, to, from);
    g_free(ctcp);
    return buf;
}

void
pecker_msg_table_build(struct pecker_conn *pecker)
{
    int i;

    if(!pecker || !pecker->msgs)
    {
        purple_debug(PURPLE_DEBUG_ERROR, "pecker", "Attempt to build a message table on a bogus structure\n");
        return;
    }

    for (i = 0; _pecker_msgs[i].name; i++)
    {
        g_hash_table_insert(pecker->msgs, (gpointer) _pecker_msgs[i].name, (gpointer) & _pecker_msgs[i]);
    }
}

void
pecker_cmd_table_build(struct pecker_conn *pecker)
{
    int i;

    if(!pecker || !pecker->cmds)
    {
        purple_debug(PURPLE_DEBUG_ERROR, "pecker", "Attempt to build a command table on a bogus structure\n");
        return;
    }

    for (i = 0; _pecker_cmds[i].name; i++)
    {
        g_hash_table_insert(pecker->cmds, (gpointer) _pecker_cmds[i].name, (gpointer) & _pecker_cmds[i]);
    }
}

char *
pecker_format(struct pecker_conn *pecker, const char *format, ...)
{
    GString *string = g_string_new("");
    char *tok, *tmp;
    const char *cur;
    va_list ap;

    va_start(ap, format);
    for (cur = format; *cur; cur++)
    {

        if(cur != format)
            g_string_append_c(string, ' ');

        tok = va_arg(ap, char *);

        switch (*cur)
        {
        case 'v':
            g_string_append(string, tok);
            break;
        case ':':
            g_string_append_c(string, ':');
            /* no break! */
        case 'n':
        case 't':
        case 'c':
            tmp = pecker_send_convert(pecker, tok);
            g_string_append(string, tmp);
            g_free(tmp);
            break;
        default:
            purple_debug(PURPLE_DEBUG_ERROR, "pecker", "Invalid format character '%c'\n", *cur);
            break;
        }
    }
    va_end(ap);
    g_string_append(string, "\r\n");
    return (g_string_free(string, FALSE));
}

void
pecker_parse_msg(struct pecker_conn *pecker, char *input)
{
    struct _pecker_msg *msgent;
    char *cur = NULL, *end = NULL, *tmp = NULL, *from = NULL, *msgname = NULL, *fmt = NULL, **args = NULL, *msg = NULL;
    guint i;

    /* purple_debug(PURPLE_DEBUG_INFO, "pecker", "RAW Protocol: %s\n", input); */

    if(!strncmp(input, "PING ", 5))
    {
        msg = pecker_format(pecker, "vv", "PONG", input + 5);
        pecker_send(pecker, msg);
        g_free(msg);
        return;
    }
    else if(!strncmp(input, "NOTIFY", 6))
    {
        pecker_notify(pecker, input);
        return;
    }
    else if(!strncmp(input, "ERROR ", 6))
    {
        purple_connection_error(purple_account_get_connection(pecker->account), "Disconnected.");
        return;
    }

    if(input[0] != ':' || (cur = strchr(input, ' ')) == NULL)
    {
        pecker_parse_error_cb(pecker, input);
        return;
    }

    from = g_strndup(&input[1], cur - &input[1]);

    cur++;
    end = strchr(cur, ' ');
    if(!end)
        end = cur + strlen(cur);
    tmp = g_strndup(cur, end - cur);
    msgname = g_ascii_strdown(tmp, -1);
    g_free(tmp);
    tmp = NULL;
    if((msgent = g_hash_table_lookup(pecker->msgs, msgname)) == NULL)
    {
        pecker_msg_default(pecker, "", from, &input);
        g_free(msgname);
        g_free(from);
        return;
    }
    g_free(msgname);

    args = g_new0(char *, strlen(msgent->format));
    for (cur = end, fmt = msgent->format, i = 0; fmt[i] && *cur++; i++)
    {

        switch (fmt[i])
        {
        case 'v':
            if(!(end = strchr(cur, ' ')))
                end = cur + strlen(cur);
            args[i] = g_strndup(cur, end - cur);
            cur += end - cur;
            break;
        case 't':
        case 'n':
        case 'c':
            if(!(end = strchr(cur, ' ')))
                end = cur + strlen(cur);
            tmp = g_strndup(cur, end - cur);
            args[i] = pecker_recv_convert(pecker, tmp);
            g_free(tmp);
            tmp = NULL;
            cur += end - cur;
            break;
        case ':':
            if(*cur == ':')
                cur++;
            args[i] = pecker_recv_convert(pecker, cur);
            cur = cur + strlen(cur);
            break;
        case '*':
            args[i] = g_strdup(cur);
            cur = cur + strlen(cur);
            break;
        default:
            purple_debug(PURPLE_DEBUG_ERROR, "pecker", "invalid message format character '%c'\n", fmt[i]);
            break;
        }
    }
    tmp = pecker_recv_convert(pecker, from);
    (msgent->cb) (pecker, msgent->name, tmp, args);
    g_free(tmp);
    for (i = 0; i < strlen(msgent->format); i++)
    {
        g_free(args[i]);
    }
    g_free(args);
    g_free(from);
}

static void
pecker_parse_error_cb(struct pecker_conn *pecker, char *input)
{
    purple_debug(PURPLE_DEBUG_WARNING, "pecker", "Unrecognized string: %s\n", input);
}

/**
 * vim:tabstop=4:shiftwidth=4:expandtab:
 */
